﻿namespace MvvmTools
{
    using System;
    using System.Runtime.CompilerServices;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics.Contracts;
    using System.Linq.Expressions;
    using JetBrains.Annotations;

    public abstract class NotificationModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Changes value and raise event "PropertyChanged" if value was changed.
        /// </summary>
        /// <param name="propertyExpression">
        /// The property expression.
        /// </param>
        /// <param name="targetValue">
        /// The target value where new value will be stored.
        /// </param>
        /// <param name="newValue">
        /// The new value.
        /// </param>
        /// <param name="equalityComparer">
        /// Equality comparer to compare target and new values.
        /// </param>
        /// <param name="onBeforeChangeCallback">
        /// An <see cref="Action"/> which called before target value modification.
        /// </param>
        /// <typeparam name="T">
        /// The type of value.
        /// </typeparam>
        /// <returns>
        /// <c>True</c> if value was changed; otherwise <c>false</c>.
        /// </returns>
        protected bool UpdateProperty<T>([NotNull] Expression<Func<T>> propertyExpression, ref T targetValue, T newValue, [CanBeNull] IEqualityComparer<T> equalityComparer = null, [CanBeNull] Action onBeforeChangeCallback = null)
        {
            Contract.Requires<ArgumentNullException>(propertyExpression != null);

            if ((equalityComparer ?? System.Collections.Generic.EqualityComparer<T>.Default).Equals(targetValue, newValue))
            {
                return false;
            }

            if (onBeforeChangeCallback != null)
            {
                onBeforeChangeCallback();
            }

            targetValue = newValue;
            OnPropertyChanged(propertyExpression);
            return true;
        }

        /// <summary>
        /// Changes value and raise event "PropertyChanged" if value was changed.
        /// </summary>
        /// <param name="targetValue">
        /// The target value where new value will be stored.
        /// </param>
        /// <param name="newValue">
        /// The new value.
        /// </param>
        /// <param name="equalityComparer">
        /// Equality comparer to compare target and new values.
        /// </param>
        /// <param name="propertyName">
        /// The property name. You could skip this parameter when use C# version 5.0 or later.
        /// </param>
        /// <typeparam name="T">
        /// The type of value.
        /// </typeparam>
        /// <returns>
        /// <c>True</c> if value was changed; otherwise <c>false</c>.
        /// </returns>
        // ReSharper disable once AssignNullToNotNullAttribute
        protected bool UpdateProperty<T>(ref T targetValue, T newValue, [CanBeNull] IEqualityComparer<T> equalityComparer = null, [CallerMemberName] [NotNull] string propertyName = null)
        {
            Contract.Requires<ArgumentNullException>(propertyName != null);
            Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(propertyName));

            if ((equalityComparer ?? System.Collections.Generic.EqualityComparer<T>.Default).Equals(targetValue, newValue))
            {
                return false;
            }

            targetValue = newValue;
            OnPropertyChanged(propertyName);
            return true;
        }

        /// <summary>
        /// Calls action to set new value and raises event "PropertyChanged" if target value differs from new value.
        /// </summary>
        /// <param name="propertyExpression">
        /// The property expression.
        /// </param>
        /// <param name="targetValue">
        /// The target value.
        /// </param>
        /// <param name="targetValueSetter">
        /// An <see cref="Action{T}"/> which called to set new value.
        /// </param>
        /// <param name="newValue">
        /// The new value.
        /// </param>
        /// <typeparam name="T">
        /// The type of value.
        /// </typeparam>
        /// <param name="equalityComparer">
        /// Equality comparer to compare target and new values.
        /// </param>
        /// <returns>
        /// <c>True</c> if target and new values differs; otherwise <c>false</c>.
        /// </returns>
        protected bool UpdateProperty<T>([NotNull] Expression<Func<T>> propertyExpression, T targetValue, [NotNull] Action<T> targetValueSetter, T newValue, [CanBeNull] IEqualityComparer<T> equalityComparer = null)
        {
            Contract.Requires<ArgumentNullException>(propertyExpression != null);
            Contract.Requires<ArgumentNullException>(targetValueSetter != null);

            if ((equalityComparer ?? System.Collections.Generic.EqualityComparer<T>.Default).Equals(targetValue, newValue))
            {
                return false;
            }

            targetValueSetter(newValue);
            OnPropertyChanged(propertyExpression);
            return true;
        }

        /// <summary>
        /// Raises event "PropertyChanged".
        /// </summary>
        /// <param name="propertyName">
        /// The property name passed in "PropertyChanged" event.
        /// </param>
        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        protected void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
        {
            var propertyName = propertyExpression.GetParameterName(GetType());
            OnPropertyChanged(propertyName);
        }
    }

}
